Skip to content

stop specializing on Copy #135634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

stop specializing on Copy #135634

wants to merge 6 commits into from

Conversation

joboet
Copy link
Member

@joboet joboet commented Jan 17, 2025

fixes #132442

std specializes on Copy to optimize certain library functions such as clone_from_slice. This is unsound, however, as the Copy implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not Copy. For instance, this code:

struct SometimesCopy<'a>(&'a Cell<bool>);

impl<'a> Clone for SometimesCopy<'a> {
    fn clone(&self) -> Self {
        self.0.set(true);
        Self(self.0)
    }
}

impl Copy for SometimesCopy<'static> {}

let clone_called = Cell::new(false);
// As SometimesCopy<'clone_called> is not 'static, this must run `clone`,
// setting the value to `true`.
let _ = [SometimesCopy(&clone_called)].clone();
assert!(clone_called.get());

should not panic, but does (playground).

To solve this, this PR introduces a new unsafe trait: TrivialClone. This trait may be implemented whenever the Clone implementation is equivalent to copying the value (so e.g. fn clone(&self) -> Self { *self }). Because of lifetime erasure, there is no way for the Clone implementation to observe lifetime bounds, meaning that even if the TrivialClone has stricter bounds than the Clone implementation, its invariant still holds. Therefore, it is sound to specialize on TrivialClone.

I've changed all Copy specializations in the standard library to specialize on TrivialClone instead. Unfortunately, the unsound #[rustc_unsafe_specialization_marker] attribute on Copy cannot be removed in this PR as hashbrown still depends on it. I'll make a PR updating hashbrown once this lands.

With Copy no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware of TrivialClone. To avoid this and restore the optimizations in most cases, I have changed the expansion of #[derive(Clone)]: Currently, whenever both Clone and Copy are derived, the clone method performs a copy of the value. With this PR, the derive macro also adds a TrivialClone implementation to make this case observable using specialization. I anticipate that most users will use #[derive(Clone, Copy)] whenever both are applicable, so most users will still profit from the library optimizations.

Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of core to implement "specialization at home", e.g. libAFL. I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the 'static bound on TypeId::of...

@rustbot
Copy link
Collaborator

rustbot commented Jan 17, 2025

r? @Mark-Simulacrum

rustbot has assigned @Mark-Simulacrum.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jan 17, 2025
@rustbot
Copy link
Collaborator

rustbot commented Jan 17, 2025

Changes to the code generated for builtin derived traits.

cc @nnethercote

@Mark-Simulacrum Mark-Simulacrum added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. I-libs-api-nominated Nominated for discussion during a libs-api team meeting. I-libs-nominated Nominated for discussion during a libs team meeting. labels Jan 17, 2025
@rust-log-analyzer

This comment has been minimized.

@Mark-Simulacrum
Copy link
Member

Going to nominate for libs-api (and libs) since this is both a breaking change (allowed since fixing soundness). I feel like I recall an RFC or some other discussion about us explicitly saying libraries shouldn't do the unsound thing here, but I don't know what that was. https://rust-lang.github.io/rfcs/1521-copy-clone-semantics.html is a bit related but not directly :)

@the8472
Copy link
Member

the8472 commented Jan 17, 2025

RFC 1521 could be interpreted so. Since it requires that Clone is equivalent to Copy when both are implemented.

Since SometimesCopy implements both (at least sometimes) they must be equivalent. And since cannot tell 'static and non-'static apart they must always be equivalent. Therefore the Clone implementation is wrong.


This is unsound, however, as the Copy implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not Copy.

I still don't think this is unsound in itself. So far all demonstrations of unsoundness required some other unsafe code to turn this into a miscompilation. E.g. the WeirdCow in #132442 or the TrustedLen impl in #89948 both require unsafe to exploit this.

Noratrieb also argues that lifetime-conditional Copy currently is unsupported in MIR.

So ISTM that this could be a documentation shortcoming and a compiler/lang issue that such implementations should be prevented but aren't.

That said, I agree that the current situation is brittle.

@scottmcm
Copy link
Member

Without saying anything about specialization on Copy, there's definitely been past land discussion of splitting the "memcpyable" part of Clone from the "don't need to write .clone()" part. Something like TrivialClone would probably be what that would need as well, and would -- as you mention in the docs in the PR -- be nice for allowing memcpying of non-Copy types like legacy::Range.

But that gets back to needing, as the8472 said, a way to actually block lifetime-bad implementations before it could be stable.

@Amanieu Amanieu removed I-libs-api-nominated Nominated for discussion during a libs-api team meeting. I-libs-nominated Nominated for discussion during a libs team meeting. labels Jan 21, 2025
@the8472
Copy link
Member

the8472 commented Jan 21, 2025

We discussed this during today's libs-API meeting. We currently are not aware of any safe code that is unsound due to these specializations and there were concerns about performance regressions for user types that manually implement Copy.

So we're leaning towards keeping the implementations as they are and instead improving things in other ways such as adding compiler warnings or improving the Copy documentation or unsafe-code-guidelines.


We'd like input from T-types whether they agree with this assessment and if something should be changed on the language side, e.g. by forbidding or at least warning on lifetime-conditional implementations, similar to how Drop impls must have the same bounds as the type it's implemented on.

A compiler-team member has indicated that lifetime-dependent Copy impls are de-facto unsupported.

@the8472 the8472 added the I-types-nominated Nominated for discussion during a types team meeting. label Jan 21, 2025
@BoxyUwU
Copy link
Member

BoxyUwU commented Jan 21, 2025

Forbidding lifetime dependent copy impls seems like it would be rather breaking (but that's pure speculation, we ought to do a crater run to check if anyone feels strongly we should forbid such impls), though generally I don't feel great about forbidding lifetime dependent copy impls. I also don't think a warning on lifetime dependent copy impls really helps anything for std as warnings cannot be relied upon for soundness and so std's usage of specialization would still be wrong.

In general I would prefer std to not be using specialization in any ways that affect behaviour in any way, it's stably exposing unstable broken parts of the type system in ways that are arguably unsound (allows you to prove trait bounds hold when they do not).

imo what should have happened is that years ago when specialization was found to be unsound all these specializations should have been ripped out regardless of the performance cost and re-added with a PR like this that respects lifetime constraints and treats the unsafe specialization marker attr as something unsafe with invariants to be upheld.

I cant speak for the whole types team but that's atleast my opinion as a types member 🤷‍♀️


On a semi-related note, does std still specialize fused iterator stuff in ways that exposes specialization to stable too? I remember that being a thing some years ago but haven't kept up to date with how std is using specialization

@the8472
Copy link
Member

the8472 commented Jan 21, 2025

On a semi-related note, does std still specialize fused iterator stuff in ways that exposes specialization to stable too?

Yes, but #86765 changed the specialization so that incorrect specializations only result in correctness issues and not soundness ones.

And we have TrusedFused now for cases where it's relevant to soundness.

@lcnr
Copy link
Contributor

lcnr commented Jan 28, 2025

The types team discussed this on zulip: https://rust-lang.zulipchat.com/#narrow/channel/326866-t-types.2Fnominated/topic/.23135634.3A.20stop.20specializing.20on.20.60Copy.60

My opinion/summary from there:

  • rn specializing on Copy is unsound from a type system pov, even as I don't know of, and can't think of, actual cases whether this results in broken invariants/ub
    • fixing specialization with lifetime dependent impls to be sound won't happen in the near future
    • forbidding lifetime dependent Copy impls is not possible/too much of a breaking change, as they are currently allowed with arbitrary where-bounds

I would like to avoid specializing on Copy. I believe we should land this PR if the approach of having a new trait implemented on derive(Copy) is good enough perf wise (whatever that means)

@lcnr lcnr removed the I-types-nominated Nominated for discussion during a types team meeting. label Jan 28, 2025
@bors
Copy link
Collaborator

bors commented Feb 2, 2025

☔ The latest upstream changes (presumably #136448) made this pull request unmergeable. Please resolve the merge conflicts.

@joboet joboet added the I-libs-nominated Nominated for discussion during a libs team meeting. label Feb 3, 2025
@cuviper
Copy link
Member

cuviper commented Feb 7, 2025

Should we add manual conditional impls for types like Option<T> and [T; N]?
And how about compiler-implemented types like closures and tuples?

I know we're not going to perfectly recover everything that Copy specialization did right, but I think these will be impactful. It's also great that we could go further, like conditional Range<T> and unconditional slice::Iter<'_, T>.

@rust-log-analyzer

This comment has been minimized.

@joboet
Copy link
Member Author

joboet commented Feb 11, 2025

Should we add manual conditional impls for types like Option<T> and [T; N]? And how about compiler-implemented types like closures and tuples?

I know we're not going to perfectly recover everything that Copy specialization did right, but I think these will be impactful. It's also great that we could go further, like conditional Range<T> and unconditional slice::Iter<'_, T>.

Maybe, but let's just try the performance of this first:
@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@joboet
Copy link
Member Author

joboet commented Mar 24, 2025

The newest commit stops the TrivialClone implementations from appearing in the docs, which should help the performance of the doc benchmarks a bit.

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Mar 24, 2025
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 24, 2025
stop specializing on `Copy`

fixes rust-lang#132442

`std` specializes on `Copy` to optimize certain library functions such as `clone_from_slice`. This is unsound, however, as the `Copy` implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not `Copy`. For instance, this code:
```rust
struct SometimesCopy<'a>(&'a Cell<bool>);

impl<'a> Clone for SometimesCopy<'a> {
    fn clone(&self) -> Self {
        self.0.set(true);
        Self(self.0)
    }
}

impl Copy for SometimesCopy<'static> {}

let clone_called = Cell::new(false);
// As SometimesCopy<'clone_called> is not 'static, this must run `clone`,
// setting the value to `true`.
let _ = [SometimesCopy(&clone_called)].clone();
assert!(clone_called.get());
```
should not panic, but does ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6be7a48cad849d8bd064491616fdb43c)).

To solve this, this PR introduces a new `unsafe` trait: `TrivialClone`. This trait may be implemented whenever the `Clone` implementation is equivalent to copying the value (so e.g. `fn clone(&self) -> Self { *self }`). Because of lifetime erasure, there is no way for the `Clone` implementation to observe lifetime bounds, meaning that even if the `TrivialClone` has stricter bounds than the `Clone` implementation, its invariant still holds. Therefore, it is sound to specialize on `TrivialClone`.

I've changed all `Copy` specializations in the standard library to specialize on `TrivialClone` instead. Unfortunately, the unsound `#[rustc_unsafe_specialization_marker]` attribute on `Copy` cannot be removed in this PR as `hashbrown` still depends on it. I'll make a PR updating `hashbrown` once this lands.

With `Copy` no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware of `TrivialClone`. To avoid this and restore the optimizations in most cases, I have changed the expansion of `#[derive(Clone)]`: Currently, whenever both `Clone` and `Copy` are derived, the `clone` method performs a copy of the value. With this PR, the derive macro also adds a `TrivialClone` implementation to make this case observable using specialization. I anticipate that most users will use `#[derive(Clone, Copy)]` whenever both are applicable, so most users will still profit from the library optimizations.

Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of `core` to implement "specialization at home", e.g. [`libAFL`](https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49). I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the `'static` bound on `TypeId::of`...
@bors
Copy link
Collaborator

bors commented Mar 24, 2025

⌛ Trying commit 1509254 with merge f7305a3...

@bors
Copy link
Collaborator

bors commented Mar 24, 2025

☀️ Try build successful - checks-actions
Build commit: f7305a3 (f7305a33c1255557ebccb7fc9ae6fb9265ba5715)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (f7305a3): comparison URL.

Overall result: ❌ regressions - please read the text below

Benchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf.

Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @rustbot label: +perf-regression-triaged along with sufficient written justification. If you cannot justify the regressions please fix the regressions and do another perf run. If the next run shows neutral or positive results, the label will be automatically removed.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

This is the most reliable metric that we have; it was used to determine the overall result at the top of this comment. However, even this metric can sometimes exhibit noise.

mean range count
Regressions ❌
(primary)
0.5% [0.1%, 1.5%] 111
Regressions ❌
(secondary)
0.9% [0.2%, 1.8%] 33
Improvements ✅
(primary)
-0.7% [-0.7%, -0.7%] 1
Improvements ✅
(secondary)
-0.7% [-0.7%, -0.7%] 1
All ❌✅ (primary) 0.5% [-0.7%, 1.5%] 112

Max RSS (memory usage)

Results (primary -0.6%, secondary 0.4%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
1.2% [0.5%, 3.3%] 9
Regressions ❌
(secondary)
1.4% [0.6%, 1.9%] 3
Improvements ✅
(primary)
-3.6% [-4.8%, -2.7%] 5
Improvements ✅
(secondary)
-2.3% [-2.3%, -2.3%] 1
All ❌✅ (primary) -0.6% [-4.8%, 3.3%] 14

Cycles

Results (primary 0.7%, secondary 2.1%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
1.1% [0.7%, 1.4%] 4
Regressions ❌
(secondary)
2.1% [2.1%, 2.1%] 1
Improvements ✅
(primary)
-1.0% [-1.0%, -1.0%] 1
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.7% [-1.0%, 1.4%] 5

Binary size

Results (primary 0.1%, secondary 0.3%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
0.1% [0.0%, 0.6%] 105
Regressions ❌
(secondary)
0.3% [0.0%, 1.5%] 59
Improvements ✅
(primary)
-0.1% [-0.2%, -0.0%] 4
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.1% [-0.2%, 0.6%] 109

Bootstrap: 774.602s -> 777.888s (0.42%)
Artifact size: 365.58 MiB -> 365.45 MiB (-0.03%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Mar 24, 2025
@Amanieu Amanieu removed T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 20, 2025
@Amanieu
Copy link
Member

Amanieu commented Apr 20, 2025

Unfortunately I think the perf cost is just something we will have to pay in the end. The more I think about this, the more I agree with @joboet that the current implementation is fundamentally unsound and we need to fix this.

Starting an FCP in case anyone from libs has objections.

@rfcbot merge

@rfcbot
Copy link
Collaborator

rfcbot commented Apr 20, 2025

Team member @Amanieu has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Apr 20, 2025
@Jules-Bertholet
Copy link
Contributor

Jules-Bertholet commented May 20, 2025

Types with Copy impls that are unsatisfied only due to lifetimes are fundamentally broken (#126179):

#[derive(Clone)]
struct Foo<'a>(&'a ());

impl Copy for Foo<'static> {}

fn foo<'a>(a: Foo<'a>) {
    let b = a; // error: lifetime may not live long enough
}

This is because MIR generation implicitly specializes on Copy.

@Jules-Bertholet
Copy link
Contributor

I think we should either decide to fix #126179, or document that lifetime-dependent Copy impls are a bug in the program and that the compiler is free to perform this sort of specialization.

@rfcbot rfcbot added the final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. label Jul 16, 2025
@rfcbot
Copy link
Collaborator

rfcbot commented Jul 16, 2025

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot removed the proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. label Jul 16, 2025
@Jules-Bertholet
Copy link
Contributor

I am still not convinced we should do this:

  • AFAIK, there is no concrete example of actual unsoundness caused by this specialization
  • Even if this PR is merged, specialization on Copy will continue to be built into the language, because of how the compiler generates MIR for moves vs copies
  • There is a significant perf cost, at compile time and also at runtime

@the8472
Copy link
Member

the8472 commented Jul 16, 2025

Even if this PR is merged, specialization on Copy will continue to be built into the language, because of how the compiler generates MIR for moves vs copies

That is a different issue though, and I have been told that it leads to the compiler erroring out later when those code paths are actually used. So unlike specialization it doesn't result in unsoundness.

AFAIK, there is no concrete example of actual unsoundness caused by this specialization

Yeah, you'd pretty much have to explicitly write unsafe code that breaks when the impl Copy where Foo<'static> vs Clone confusion is involved. We could blame that unsafe code or the impl for that, but Copy is a safe trait so formally the unsafe code wouldn't have made an incorrect assumption that a Clone should have happened instead of Copy. So de jure this is unsound.

There is a significant perf cost, at compile time and also at runtime

That's my biggest concern too. Personally I'd prefer if we could keep the perf, but general principles are that soundness beats perf (... tell me it's not true and we'll find things that can be made O(1) by inserting "reasonable assumptions" in various places 😛).

If we could make those Copy impls illegal then that'd be great and we could reject this PR, but AIUI that's equivalent to solving the general specialization soundness hole because due to bounds on associated types we can't generally prove that no indirectly-lifetime-dependent impls exist.

@Jules-Bertholet
Copy link
Contributor

We could blame that unsafe code or the impl for that, but Copy is a safe trait so formally the unsafe code wouldn't have made an incorrect assumption that a Clone should have happened instead of Copy. So de jure this is unsound.

We could simply declare this assumption false, and clearly document that it is false. E.g., add this paragraph to the Copy docs:

The compiler, as well as libraries, are free to assume that any type that is identical-modulo-lifetimes to a Copy type may itself be freely copied.

And this paragraph to the Clone docs:

If a type T is eligible to be freely copied (implements Copy, or identical-modulo-lifetimes to a Copy type), then the compiler, as well as libraries, are free to replace any call to <T as Clone>::clone or <T as Clone>::clone_from with such a copy.

There is no known existing code out there that relies on that not being true. And if it’s all clearly documented, then such code won’t appear in the future, either.


Another consideration is the upcoming const Trait feature (rust-lang/rfcs#3762). Specifically, it would be really nice if Copy had const Clone as a supertrait. For most traits, there would be no possible way of making that happen backward-compatibly. But with explicit disclaimers like the ones I suggest above, it should be possible to hack it into working.

@the8472
Copy link
Member

the8472 commented Jul 16, 2025

If a type T is eligible to be freely copied (implements Copy, or identical-modulo-lifetimes to a Copy type),

If we can't prove this property then users will also have a hard time evaluating that when faced with fun indirections like impl<T> Copy for Foo<T> where T: Bar, <T as Bar>::Baz: Copy.

Just saying "don't do that" without enforcement seems pretty iffy. @Amanieu probably has stronger opinions on that.

Comment on lines +413 to 415
// This is unsound, but required by `hashbrown`
// FIXME(joboet): change `hashbrown` to use `TrivialClone`
#[rustc_unsafe_specialization_marker]
Copy link
Member

@cuviper cuviper Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to highlight that this PR does not completely remove Copy specialization yet, but I think we should consider the FCP as the larger intent to eventually remove it all.

There are also a couple NonDrop traits that are piggy-backing on Copy and will need to stop or find another means. I think those are sound as-is though, since an actual Drop can't be lifetime-dependent. (edit: actually, those may be fine even if Copy itself loses the marker?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least the vec::IntoIter NonDrop is also a rustc_unsafe_specialization_marker which would allow it to continue to exist.

The unstable sort impls also have a specialization relating to Copy and Freeze, I'm not sure what they're doing... I think they make it safe to look at multiple copies of the value instead of moving it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have a measurement of the full performance cost of removing all Copy specialization before making the decision?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. perf-regression Performance regression. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Array and Vec's Clone specialization is maybe unsound with conditionally Copy types.